package com.stars.easyms.redis.initializer;

import com.stars.easyms.base.constant.EasyMsCommonConstants;
import com.stars.easyms.redis.annotion.*;
import com.stars.easyms.redis.exception.RedisException;
import com.stars.easyms.redis.holder.RedisInfoHolder;
import com.stars.easyms.redis.holder.RedisMethodInfo;
import com.stars.easyms.redis.properties.EasyMsRedisProperties;
import com.stars.easyms.base.util.ExecutorUtil;
import com.stars.easyms.base.util.ReflectUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Redis服务初始化启动类
 *
 * @author guoguifang
 * @date 2018-04-23 13:54
 * @since 1.0.0
 */
public final class RedisManagerInitializer {

    private static Logger logger = LoggerFactory.getLogger(RedisManagerInitializer.class);

    private static AtomicBoolean isInit = new AtomicBoolean(false);

    private EasyMsRedisProperties easyMsRedisProperties;

    public void init() {
        // 判断是否异步启动
        if (easyMsRedisProperties.isAsyncLoad()) {
            ExecutorUtil.execute(this::initRedis);
        } else {
            initRedis();
        }
    }

    public void initRedis() {
        if (isInit.compareAndSet(false, true)) {
            logger.info("Begin to register redis service......");
            try {
                this.executeInit();
                logger.info("Register redis service success!");
            } catch (RedisException e) {
                logger.error("Register redis service fail!", e);
                System.exit(1);
            }
        }
    }

    private void executeInit() throws RedisException {
        // 获取所有加了redis注解的方法
        Class<?>[] annotationArray = new Class[]{RedisStringSet.class, RedisStringGet.class,
                RedisHashSet.class, RedisHashGet.class, RedisHashLength.class,
                RedisDelete.class, RedisEval.class, RedisExists.class, RedisExpire.class, RedisTtl.class,
                RedisSetAdd.class, RedisSetMembers.class,
                RedisListRange.class, RedisListPush.class, RedisListPop.class, RedisListLength.class};
        Set<Method> redisMethodSet = ReflectUtil.getAllMethodByAnnotationWithDefaultAll(
                easyMsRedisProperties.getBasePackage(), annotationArray);
        if (redisMethodSet.isEmpty()) {
            logger.debug("Effective redis usage method was not found!");
            return;
        }

        String prefix = easyMsRedisProperties.getPrefix();
        // 遍历redis方法获取redisInfo
        for (Method redisMethod : redisMethodSet) {
            // 因为redis注解使用了aop，因此必须符合代理类的规则，该方法必须是public的
            String redisMethodFullName = ReflectUtil.getMethodFullName(redisMethod);
            if (!Modifier.isPublic(redisMethod.getModifiers())) {
                throw new RedisException("Method({}) modifier is not public!", redisMethodFullName);
            }

            // 获取redis方法的类，判断是否有RedisService注解，若有则获取该类默认的group
            String group = null;
            Class<?> redisServiceClass = redisMethod.getDeclaringClass();
            if (redisServiceClass.isAnnotationPresent(RedisManager.class)) {
                RedisManager redisManager = redisServiceClass.getAnnotation(RedisManager.class);
                if (StringUtils.isNotBlank(redisManager.group())) {
                    group = redisManager.group();
                }
            }

            // 每个方法只能拥有一个redis注解，判断redis注解数量若不为1则抛出异常
            int annotationCount = 0;
            Class<? extends Annotation> redisAnnotation = null;
            String redisAnnotationString = null;
            Boolean hasPrefix = null;
            if (redisMethod.isAnnotationPresent(RedisStringSet.class)) {
                RedisStringSet redisStringSet = redisMethod.getAnnotation(RedisStringSet.class);
                hasPrefix = redisStringSet.prefix();
                group = getGroup(redisStringSet.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisStringSet.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisStringSet, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisStringGet.class)) {
                RedisStringGet redisStringGet = redisMethod.getAnnotation(RedisStringGet.class);
                hasPrefix = redisStringGet.prefix();
                group = getGroup(redisStringGet.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisStringGet.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisStringGet, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisHashSet.class)) {
                RedisHashSet redisHashSet = redisMethod.getAnnotation(RedisHashSet.class);
                hasPrefix = redisHashSet.prefix();
                group = getGroup(redisHashSet.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisHashSet.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisHashSet, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisHashGet.class)) {
                RedisHashGet redisHashGet = redisMethod.getAnnotation(RedisHashGet.class);
                hasPrefix = redisHashGet.prefix();
                group = getGroup(redisHashGet.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisHashGet.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisHashGet, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisHashLength.class)) {
                Class<?> returnType = redisMethod.getReturnType();
                if (!Long.class.equals(returnType) && !Integer.class.equals(returnType)) {
                    throw new RedisException("Method({}) annotation({}) returnType should be Long or Integer!",
                            redisMethodFullName, RedisHashLength.class.getName());
                }
                RedisHashLength redisHashLength = redisMethod.getAnnotation(RedisHashLength.class);
                hasPrefix = redisHashLength.prefix();
                group = getGroup(redisHashLength.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisHashLength.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisHashLength, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisDelete.class)) {
                RedisDelete redisDelete = redisMethod.getAnnotation(RedisDelete.class);
                hasPrefix = redisDelete.prefix();
                group = getGroup(redisDelete.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisDelete.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisDelete, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisEval.class)) {
                Class<?> returnType = redisMethod.getReturnType();
                if (!String.class.isAssignableFrom(returnType)) {
                    throw new RedisException("Method({}) annotation({}) returnType should be java.lang.String!",
                            redisMethodFullName, RedisEval.class.getName());
                }
                redisAnnotation = RedisEval.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation,
                        redisMethod.getAnnotation(RedisEval.class), group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisExists.class)) {
                Class<?> returnType = redisMethod.getReturnType();
                if (!Boolean.class.isAssignableFrom(returnType) &&
                        !boolean.class.isAssignableFrom(returnType) && !Map.class.isAssignableFrom(returnType)) {
                    throw new RedisException("Method({}) annotation({}) returnType should be Boolean or boolean or Map<String, Boolean>!",
                            redisMethodFullName, RedisExists.class.getName());
                }
                RedisExists redisExists = redisMethod.getAnnotation(RedisExists.class);
                hasPrefix = redisExists.prefix();
                group = getGroup(redisExists.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisExists.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisExists, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisExpire.class)) {
                Class<?> returnType = redisMethod.getReturnType();
                if (!Long.class.isAssignableFrom(returnType) && !long.class.isAssignableFrom(returnType)) {
                    throw new RedisException("Method({}) annotation({}) returnType should be Long or long!",
                            redisMethodFullName, RedisExpire.class.getName());
                }
                RedisExpire redisExpire = redisMethod.getAnnotation(RedisExpire.class);
                hasPrefix = redisExpire.prefix();
                group = getGroup(redisExpire.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisExpire.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisExpire, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisTtl.class)) {
                Class<?> returnType = redisMethod.getReturnType();
                if (!Long.class.isAssignableFrom(returnType) && !long.class.isAssignableFrom(returnType)) {
                    throw new RedisException("Method({}) annotation({}) returnType should be Long or long!",
                            redisMethodFullName, RedisTtl.class.getName());
                }
                RedisTtl redisTtl = redisMethod.getAnnotation(RedisTtl.class);
                hasPrefix = redisTtl.prefix();
                group = getGroup(redisTtl.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisTtl.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisTtl, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisListPush.class)) {
                RedisListPush redisListPush = redisMethod.getAnnotation(RedisListPush.class);
                hasPrefix = redisListPush.prefix();
                group = getGroup(redisListPush.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisListPush.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisListPush, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisListRange.class)) {
                Class<?> returnType = redisMethod.getReturnType();
                if (!List.class.isAssignableFrom(returnType)) {
                    throw new RedisException("Method({}) annotation({}) returnType should be List!",
                            redisMethodFullName, RedisListRange.class.getName());
                }
                RedisListRange redisListRange = redisMethod.getAnnotation(RedisListRange.class);
                hasPrefix = redisListRange.prefix();
                group = getGroup(redisListRange.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisListRange.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisListRange, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisListPop.class)) {
                RedisListPop redisListRange = redisMethod.getAnnotation(RedisListPop.class);
                hasPrefix = redisListRange.prefix();
                group = getGroup(redisListRange.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisListPop.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisListRange, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisListLength.class)) {
                Class<?> returnType = redisMethod.getReturnType();
                if (!Long.class.equals(returnType) && !Integer.class.equals(returnType)) {
                    throw new RedisException("Method({}) annotation({}) returnType should be Long or Integer!",
                            redisMethodFullName, RedisListLength.class.getName());
                }
                RedisListLength redisListRange = redisMethod.getAnnotation(RedisListLength.class);
                hasPrefix = redisListRange.prefix();
                group = getGroup(redisListRange.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisListLength.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisListRange, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisSetAdd.class)) {
                RedisSetAdd redisSetAdd = redisMethod.getAnnotation(RedisSetAdd.class);
                hasPrefix = redisSetAdd.prefix();
                group = getGroup(redisSetAdd.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisSetAdd.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisSetAdd, group);
                annotationCount++;
            }
            if (redisMethod.isAnnotationPresent(RedisSetMembers.class)) {
                RedisSetMembers redisSetMembers = redisMethod.getAnnotation(RedisSetMembers.class);
                hasPrefix = redisSetMembers.prefix();
                group = getGroup(redisSetMembers.group(), group, hasPrefix, prefix);
                redisAnnotation = RedisSetMembers.class;
                redisAnnotationString = getRedisAnnotationString(redisAnnotation, redisSetMembers, group);
                annotationCount++;
            }
            if (redisAnnotation == null || annotationCount > 1) {
                throw new RedisException("Method({}) annotation can only have one redis annotation!", redisMethodFullName);
            }
            if (redisAnnotation != RedisEval.class && group == null) {
                throw new RedisException("Method({}) annotation must give the group a value!", redisMethodFullName);
            }

            // 创建Redis方法信息并保存进内存中
            RedisMethodInfo redisMethodInfo = new RedisMethodInfo();
            redisMethodInfo.setFullMethodName(redisMethodFullName);
            redisMethodInfo.setRedisAnnotation(redisAnnotation);
            redisMethodInfo.setMethod(redisMethod);
            redisMethodInfo.setRedisAnnotationString(redisAnnotationString);
            if (hasPrefix == null) {
                logger.info("Found redis method: {} ==> redisType:{}", redisMethodFullName, redisAnnotation.getSimpleName());
                RedisInfoHolder.putScriptRedisMethod(redisMethodInfo);
            } else {
                logger.info("Found redis method: {} ==> redisType:{}, group:{}", redisMethodFullName, redisAnnotation.getSimpleName(), group);
                RedisInfoHolder.putRedisMethod(hasPrefix && StringUtils.isNotBlank(prefix) ? prefix : "", group, redisMethodInfo);
            }
        }
    }

    public boolean isInit() {
        return isInit.get();
    }

    private String getGroup(String methodGroup, String classGroup, boolean hasPrefix, String prefix) {
        String group = StringUtils.isNotBlank(methodGroup) ? methodGroup : classGroup;
        if (hasPrefix && StringUtils.isNotBlank(prefix)) {
            return StringUtils.join(new Object[]{prefix, group}, EasyMsCommonConstants.REDIS_KEY_SEPARATOR);
        }
        return group;
    }

    private String getRedisAnnotationString(Class<? extends Annotation> redisAnnotationClass, Annotation annotation, String group) throws RedisException {
        StringBuilder sb = new StringBuilder("@").append(redisAnnotationClass.getSimpleName()).append("(");
        Field[] fields = redisAnnotationClass.getDeclaredFields();
        for (int i = 0, length = fields.length; i < length; i++) {
            Field field = fields[i];
            field.setAccessible(true);
            String fieldName = field.getName();
            sb.append(fieldName).append(" = ");
            if ("group".equals(fieldName)) {
                sb.append("\"").append(group).append("\"");
            } else {
                Class<?> fieldClass = field.getType();
                try {
                    if (fieldClass.equals(String.class)) {
                        sb.append("\"").append(field.get(annotation)).append("\"");
                    } else {
                        sb.append(field.get(annotation));
                    }
                } catch (IllegalAccessException e) {
                    throw new RedisException("Annotation {} get field {} failure!", redisAnnotationClass.getName(), fieldName);
                }
            }
            if (i != length - 1) {
                sb.append(", ");
            }
        }
        return sb.append(")").toString();
    }

    public RedisManagerInitializer(EasyMsRedisProperties easyMsRedisProperties) {
        this.easyMsRedisProperties = easyMsRedisProperties;
    }
}
